<!--
  该组件必须放在调用v-visual-tooltip指令前面，不然会报错
  使用实例：
    <Tooltip :componentStyle="componentStyle" ref="Tooltip"/>
    <div v-visual-tooltip="{config:property.tooltip, vars, extVars}">...</div>
-->
<template>
  <div
    v-if="!isVisualH5"
    ref="tipBoxWrap"
    :class="`tooltip`"
  >
    <div
      v-show="isShow && html"
      ref="tipBox"
      :class="`tipBox ${componentStyle}`"
      :style="tooltipStyle"
    >
      <div
        v-loading="isLoading"
        element-loading-background="rgba(53, 83, 107, 1)"
      >
        <TinymceEditorHtml
          :html="html"
          :component-style="componentStyle"
        />
      </div>
    </div>
  </div>
</template>
<script>
import TinymceEditorHtml from '@/components/common/tinymce-editor/tinymce-editor-html.vue';
import { addCommonParamsBySourceType, getMatchCommonEvents, getMatchCommonParams } from '@/components/multi-params/multi-params-util.js';
import baseFunctions from '@/mixin/base-functions.js';
import dataModel from '@/models/panel-config-data.js';
import {
  dateFormat, evalFormula,
  isArray, isEmpty,
  isObject, matchAll,
  objHas, permil,
  refactorApis, xssValueFilter,
} from '@/utils/util';
import { getVarChildVal } from '@/utils/util-common.js';
import { isNum } from '@/utils/util.js';
import whiteList from '@/utils/xss/tag-white-list';
import { mapMutations, mapState } from 'vuex';
import xss from 'xss';
import { nextTick } from 'vue';

const getSelfComp = binding => {
  const { parentComp } = binding.value;
  const tooltipVm = parentComp.$refs.Tooltip;
  return tooltipVm;
};

const bindFun = (el, binding) => {
  el.onmouseenter = e => {
    const self = getSelfComp(binding);
    self.parentComp = binding.value.parentComp;
    self.config = binding.value.config;
    self.setState({ tooltipOnWid: self.parentComp.widgetInstanceId });
    if (!self.checkAvaible()) {
      return;
    }

    self.vars = binding.value.vars;
    self.extVars = binding.value.extVars ? binding.value.extVars : {};
    self.setVarsFromChart();
    self.richText = xssValueFilter(self.config.html);
    self._bindDirectiveOnmouseenter(self, binding, e);
  };

  el.onmousemove = e => {
    const self = getSelfComp(binding);
    if (!self.checkAvaible()) {
      return;
    }
    self.resetPosition(e, self);
  };

  el.onmouseleave = e => {
    const self = getSelfComp(binding);
    self.setState({ tooltipOnWid: '' });
    self.hideTip();
  };
};

export default {
  name: 'ToolTip',
  components: { TinymceEditorHtml },
  mixins: [baseFunctions],
  props: {
    componentStyle: {
      type: String,
      default: 'light', // light | dark
    },
    type: {
      type: String,
      defaut: '', // '' | echart
    },
    tooltipParams: Object,
  },
  data() {
    return {
      config: {}, // 配置，对应目标组件config.js下的property.tooltip属性
      tooltipConfig: {}, // 单个tooltip配置
      vars: {}, // 主要变量
      extVars: {}, // 扩展变量

      /* ---------------*/
      tipBoxId: 'visual-rich-tip-box',
      isTipBoxCreated: false,
      x: 0,
      y: 0,
      isShow: false,
      html: '',
      apiReadyCount: 0, // 已查询完毕的接口数量
      apiVars: [],
      apiDatas: {},
      params: {
        params: [],
      },
      showTipTimer: null,
      parentComp: null,
      chartSerieItem: {

        // echart图表经过单个系列时获取到的数据
        dataIndex: 0, // 数据组索引
        name: '', // 可能是x轴名称 '2020-1-1'
        seriesIndex: 0, // 系列索引id
        seriesName: '', // 系列名，'吉利莲'
        seriesType: '', // 系列类型，'bar'
        value: '', // 值
        type: '', // mouseover | mouseout
      },
      mapItem: {}, // 通用地图数据
      dataVars: {}, // 父组件接口单条数据
      richText: '', // 后台校验通过后的富文本
      isSpecialTips: false,
      isUpdateCoordinate: true, // 是否更新x,y
    };
  },
  computed: {
    ...mapState({
      asTooltips: state => state.VisualComponentsModule.asTooltips,
      asTooltipUsers: state => state.VisualComponentsModule.asTooltipUsers,
      tooltipOnWid: state => state.VisualComponentsModule.tooltipOnWid,
    }),
    boxStyle() {
      return {
        position: 'fixed',
        zIndex: 10000,
        left: `${this.x}px`,
        top: `${this.y}px`,
      };
    },
    isLoading() {
      if (!this.config) {
        return true;
      }
      if (!this.config.apis) {
        return true;
      }
      if (this.config.apis.length === 0) {
        return false;
      }
      if (this.apiReadyCount >= Object.keys(this.apiDatas).length) {
        return false;
      }
      return true;
    },
    tooltipStyle() {
      let _config = {};
      if (!this.isSpecialTips) {
        _config = JSON.parse(JSON.stringify(this.config));
      } else {
        _config = JSON.parse(JSON.stringify(this.tooltipConfig));
      }
      return _config.advancedTooltipSetting && typeof _config.advancedTooltipSetting === 'object' ?
        { ...this.boxStyle, ...this.getCssStyle(_config.advancedTooltipSetting) } :
        this.boxStyle;
    },
  },
  watch: {
    apiReadyCount(count, oldCount) {
      let allReady = true;
      for (const i in this.apiDatas) {
        if (!this.apiDatas[i].result) {
          allReady = false;
        }
      }
      if (allReady) {
        this.getApiVars();
        this.replaceHtml();
      }
    },
    chartSerieItem(item) {
      const { isSaved, isOriginVisiable } = item?.data?.tooltipConfig || {};
      if (isSaved && !isOriginVisiable) {
        this.tooltipConfig = JSON.parse(JSON.stringify(item.data.tooltipConfig));
        this.isSpecialTips = true;
      } else {
        this.isSpecialTips = false;
        this.tooltipConfig = {};
      }
      if (!this.checkAvaible()) {
        return;
      }
      if (item.type === 'mouseout') {
        clearTimeout(this.showTipTimer);
        this.isUpdateCoordinate = false;
        let { hideDelay } = this.config || {}; // hideDelay：高级tooltip浮层隐藏的延迟
        hideDelay = isNaN(hideDelay * 1000) ? 100 : hideDelay * 1000;
        this.showTipTimer = setTimeout(() => {
          if (window.visualComp.tooltipEvtType === 'mouseout') {
            this.isShow = false;
          }
          if (this.type === 'vxeTable') {
            this.isShow = false;
          }
        }, hideDelay);
      } else {
        clearTimeout(this.showTipTimer);
        this.isUpdateCoordinate = true;
        this.isShow = true;
        this.setVarsFromChart();
        this.getApiVars();
        this.replaceHtml();
        this.toogleTipByHtml();
      }
    },
    mapItem(item) {
      if (!this.checkAvaible()) {
        return;
      }
      if (!item) {
        this.isShow = false;
      } else {
        clearTimeout(this.showTipTimer);
        this.isUpdateCoordinate = true;
        this.isShow = true;
        this.setVarsFromMap();
        this.getApiVars();
        this.replaceHtml();
      }
    },
    dataVars(dataVars) {
      if (dataVars) {
        this.vars.dataVars = dataVars;
      }
      this.setVarsFromChart();
      this.getApiVars();
      this.replaceHtml();
      this.toogleTipByHtml();
    },
    tooltipParams() {
      this.bindParams();
    },

    isShow() {
      if (this.isShow) {
        nextTick(() => {
          this.replaceAsTooltip();
        });
      }
    },
    tooltipOnWid(newWid, oldWid) {
      if (this.parentComp && newWid !== this.parentComp.widgetInstanceId) {
        this.hideTip();
      }
    },
  },
  beforeUnmount() {
    this.parentComp = null;
    this.hideTip(true);
  },
  methods: {
    ...mapMutations(['setState', 'setAsTooltip', 'setAsTooltipUserVars']),
    setVarsFromChart() {
      if (this.type === 'echart') {
        const { data, dataIndex, seriesIndex, seriesName, seriesType, value } = this.chartSerieItem;
        let { name } = this.chartSerieItem;

        /*
         * 针对x轴连续
         * 饼图需要按照正值展示，tooltip展示原始数据
         */
        let _value = data && data.trueValue ? data.trueValue : value;
        const arr = ['bar', 'line'];
        if (arr.includes(seriesType) && isArray(value)) {
          name = name || value[0];
          _value = value[1];
        }

        this.vars = {
          ...this.vars,
          dataIndex,
          name,
          seriesIndex,
          seriesName,
          seriesType,
          value: _value,
        };
      } else if (this.type === 'vxeTable') {
        this.vars = this.chartSerieItem;
      } else {}
    },

    setVarsFromMap() {
      this.vars = this.mapItem;
    },

    _bindDirectiveOnmouseenter(self, binding, e) {
      self.bindParams(binding);
      if (self.config.apis.length === 0) {
        self.replaceHtml();
      } else {
        self.html = 'Loading...';
      }
      self.getApiDatas();
      self.showTip(e, self);

      if (self.type === 'echart' || self.type === 'vxeTable') {
        self.isShow = false;
      }
    },
    resetPosition(e, self) {
      const x = e.clientX;
      const y = e.clientY;

      // 计算浮动框位置
      const { tipBox } = self.$refs; // 浮动box
      const tipBoxRect = tipBox.getBoundingClientRect();
      const winW = window.innerWidth;
      const winH = window.innerHeight;
      if (!this.isUpdateCoordinate) {
        return;
      }
      if (x + tipBoxRect.width > winW && y + tipBoxRect.height > winH) {
        self.x = x - tipBoxRect.width - 10;
        self.y = y - tipBoxRect.height - 10;
      } else {
        if (x + tipBoxRect.width > winW) {
          self.x = winW - tipBoxRect.width;
        } else {
          self.x = x + 10;
        }
        if (y + tipBoxRect.height > winH) {
          self.y = winH - tipBoxRect.height;
        } else {
          self.y = y + 10;
        }
      }
    },

    // 绑定传参
    bindParams(binding) {
      let params = {};
      let parentComp;

      if (this.tooltipParams) {
        params = JSON.parse(JSON.stringify(this.tooltipParams));
      } else if (binding && binding.value && binding.value.parentComp) {
        parentComp = binding.value.parentComp;
        params = parentComp.params;
      } else {}

      if (Array.isArray(params)) {
        // x轴公用组件
        params = params[0];
      }

      params = JSON.parse(JSON.stringify(params || {}));
      if (!params.params) {
        params.params = [];
      }

      // 补充公共参数
      if (parentComp) {
        const { events } = parentComp.config;
        if (!events) {
          return;
        }
        const matchEvents = getMatchCommonEvents(events);
        const matchParams = getMatchCommonParams(matchEvents);

        params.params = addCommonParamsBySourceType('xMiner', params.params, matchParams);
      }

      const flag = JSON.stringify(params.params) === JSON.stringify(this.params.params);
      if (!flag) {
        this.params = JSON.parse(JSON.stringify(params));
        const apis = this.config.apis || [];
        for (let i = 0; i < apis.length; i++) {
          const key = this.getApiKey(apis[i]);
          if (this.apiDatas[key]) {
            this.apiDatas[key].result = null;
          }
        }
      }
    },

    checkAvaible() {
      if (this.isSpecialTips) {
        return this.tooltipConfig &&
          this.tooltipConfig.isVisiable &&
          this.tooltipConfig.html ?
          true : false;
      }
      if (!this.config) {
        return false;
      }
      if (!this.config.isVisiable) {
        return false;
      }
      if (!this.config.html) {
        return false;
      }
      return true;
    },

    replace(html, vars, fixEmpty = true) {
      const _html = html.replace(/\$\{([^\}]+)\}/g, (s, n) => {
        let _n = n;
        if (/[\\+\\-\\*\/%]/.test(_n)) {
          // 计算公式 ${a+b[1]*d-e/f%10}
          const arr = _n
            .replace(/[\\+\\-\\*\/%\\{\\}\\(\\)]/g, ',')
            .replace(/\s*/g, '')
            .split(',')
            .filter(v => v);
          for (let i = 0; i < arr.length; i++) {
            if (isNaN(arr[i])) {
              let val = this.getVarChildVal(vars, arr[i]);
              if (isEmpty(val)) {
                val = '';
              }
              val = (`${val}`).replace(/,/g, ''); // 1,500 => 1500
              let tryCount = 10;
              while (_n.indexOf(arr[i]) !== -1 && tryCount > 0) {
                tryCount--;
                _n = _n.replace(arr[i], val);
              }
            }
          }
          _n = evalFormula(_n);
          if (fixEmpty && isEmpty(_n)) {
            _n = '';
          }
          return _n;
        } else {
          // 普通变量 ${a}
          let value = this.getVarChildVal(vars, _n);
          value = this.getValStr(value);
          if (fixEmpty && isEmpty(value)) {
            value = '';
          }
          return value;
        }
      });
      return _html;
    },

    // 替换模板中的条件代码块
    replaceIfBlocks(html, vars) {
      const _html = html.replace(/#--- if (\$\{[^#]*\}) ---#([^#]*)#--- end if ---#/g, n => {
        let nHtml = '';
        const arr1 = matchAll(n, /#--- if (\$\{[^#]*\}) ---#([^#]*)#--- end if ---#/g, 1);
        const arr2 = matchAll(n, /#--- if (\$\{[^#]*\}) ---#([^#]*)#--- end if ---#/g, 2);
        const ifTrue = this.replace(arr1[0], vars, true); // if条件是否为真
        if (ifTrue) {
          nHtml = this.replace(arr2[0], vars);
        }

        return nHtml;
      });
      return _html;
    },

    // 使用xss过滤对象中的值
    xssFilter(obj, key = '', pObj = {}) {
      if (isArray(obj)) {
        for (let i = 0; i < obj.length; i++) {
          this.xssFilter(obj[i], i, obj);
        }
      } else if (isObject(obj)) {
        Object.entries(obj).map(([i]) => {
          this.xssFilter(obj[i], i, obj);
        });
      } else {
        if (typeof obj === 'string') {
          pObj[key] = xss(obj, {
            whiteList,
          });
        }
      }
      return obj;
    },
    replaceHtml() {
      let html = this.richText || '';
      if (this.isSpecialTips) {
        html = this.tooltipConfig?.html || '';
      }
      const vars = { ...this.apiVars, ...this.vars, ...this.extVars };
      this.xssFilter(vars);
      html = this.replaceIfBlocks(html, vars);
      html = this.replace(html, vars);
      html = html.replace(/\${[^}]+}/g, '');
      if (this.config.isPermil) {
        html = permil(html);
      }
      html = html.replace(/[\#\@]{1}{([^}]+)}/g, (s, s1) => `<span class="asTooltip asTooltip_${s1}">${s1}</span>`);
      this.html = xssValueFilter(html, 'editor');
      nextTick(() => {
        this.setAsTooltipUserVars({
          wid: this.parentComp?.widgetInstanceId,
          vars,
        });
        this.replaceAsTooltip();
      });
    },

    getVarChildVal(vars, keyPath) {
      let value;
      if (objHas(vars, keyPath)) {
        value = vars[keyPath];
      } else if (/^v\d+\./.test(keyPath)) {
        // v1.利润
        value = vars[keyPath];
      } else {
        value = getVarChildVal(vars, keyPath);
      }
      return value;
    },
    getVarDataType(_var) {
      // Object, Array, String, Undefined
      const type = Object.prototype.toString.call(_var).replace(/\[object (.*)\]/, '$1');
      return type;
    },
    getValStr(value) {
      let _value = value;
      const type = this.getVarDataType(_value);
      const hasTime = x => x?.toString?.()?.includes?.('GMT+') || false;
      if (type === 'Array' || type === 'Object') {
        if (type === 'Array' && value.some(item => hasTime(item))) {
          value.forEach((item, index) => {
            if (hasTime(item)) {
              const it = dateFormat('yyyy-MM-dd hh:mm:ss', new Date(item.toString()));
              _value[index] = it;
            }
          });
        } else {
          _value = JSON.stringify(value);
        }
      }

      if (isNum(_value)) {
        _value = `${_value}`;
      } else {
        _value = _value ? `${_value}` : '';
      }
      if (_value.indexOf('GMT+') !== -1) {
        _value = dateFormat('yyyy-MM-dd hh:mm:ss', new Date(_value));
      }

      return _value;
    },
    showTip(e, self) {
      clearTimeout(this.showTipTimer);
      document.body.appendChild(this.$refs.tipBox);
      this.resetPosition(e, self);
      this.toogleTipByHtml();
    },
    hideTip(immediately = false) {
      if (this.isVisualH5) {
        return;
      }
      clearTimeout(this.showTipTimer);
      const fn = () => {
        this.isShow = false;
        this.$refs?.tipBoxWrap?.appendChild(this.$refs?.tipBox);
      };
      if (immediately) {
        fn();
      } else {
        let { hideDelay } = this.config || {};
        this.isUpdateCoordinate = false;
        hideDelay = isNaN(hideDelay * 1000) ? 30 : hideDelay * 1000;
        this.showTipTimer = setTimeout(fn, hideDelay);
      }
    },
    toogleTipByHtml() {
      let temp = document.createElement('div');
      temp.innerHTML = xssValueFilter(this.html, 'editor');
      const content = temp.innerText.trim();
      if (content) {
        clearTimeout(this.showTipTimer);
        this.isUpdateCoordinate = true;
        this.isShow = true;
      } else {
        this.isShow = false;
      }
      temp = null;
    },
    getApiVars() {
      const mainVars = { ...this.vars, ...this.extVars };
      let apiVars = {};
      const apisList = this.config?.apis || [];
      for (let i = 0; i < apisList.length; i++) {
        const api = this.config.apis[i];
        const key = this.getApiKey(api);
        const res = this.apiDatas[key] && this.apiDatas[key].result;
        let vars;

        // 有数据关联
        if (api.correlation === 'correlate') {
          if (res) {
            vars = res.find(r => {
              let flag = true;
              api.correlatedData.forEach(d => {
                const val = getVarChildVal(mainVars, d.priKey);
                if (r[d.priMapKey] !== val) {
                  flag = false;
                }
              });
              return flag;
            });
          }
        } else {
          vars = res && res.length && res[0];
        }
        const newVars = {};
        if (vars) {
          Object.entries(vars).map(([varKey, val]) => {
            const field = api.apiFields.find(f => f.option_value === varKey);
            if (field && field.option_label) {
              const key1 = `v${i + 1}.${field.option_label}`; // v1.名称(新) v1.name (旧)
              newVars[key1] = vars[varKey];
            }
          });
        }
        apiVars = { ...apiVars, ...newVars };
      }

      this.apiVars = apiVars;
    },
    getApiKey(api) {
      return `${api.dataSourceType}__${api.dataSource}__${api.apiCode}`;
    },
    getPropertyMapping(api) {
      const map = {};
      api.apiFields.forEach(n => {
        map[n.option_value] = n.option_value;
      });
      return map;
    },
    initApiDatas() {
      const { apis } = this.config;
      for (let i = 0; i < apis.length; i++) {
        const key = this.getApiKey(apis[i]);
        if (!this.apiDatas[key]) {
          this.apiDatas[key] = {
            ...apis[i],
            result: null,
          };
        } else {
          this.apiDatas[key] = {
            ...this.apiDatas[key],
            ...apis[i],
          };
        }
      }

      Object.entries(this.apiDatas).map(([i]) => {
        const api = this.apiDatas[i];
        const hasApi = apis.some(n => n.dataSourceType === api.dataSourceType && n.dataSource === api.dataSource && n.apiCode === api.apiCode);
        if (!hasApi) {
          delete this.apiDatas[i];
        }
      });
    },
    addReqParamsToXvue(params = {}) {
      const _params = JSON.parse(JSON.stringify(params));
      let { $xvuePageComponentApiMap: xvueApiMap } = app.config.globalProperties;
      if (!xvueApiMap || !Array.isArray(xvueApiMap)) {
        xvueApiMap = [];
      }
      const instanceId = this.parentComp && this.parentComp.widgetInstanceId;
      let { curComp } = this.parentComp || {};
      curComp = curComp ? `${curComp}_tooltip` : 'tooltip';
      const apis = curComp ? { ..._params, curComp } : _params;
      delete apis.propertyMapping;
      const targetInstanceIndex = xvueApiMap.findIndex(item => item.instanceId === instanceId);
      const refactorApi = refactorApis(apis);
      if (targetInstanceIndex === -1) {
        xvueApiMap.push({ instanceId, apis: [refactorApi] });
      } else {
        const curApis = xvueApiMap[targetInstanceIndex]['apis'];
        if (!Array.isArray(curApis)) {
          xvueApiMap[targetInstanceIndex]['apis'] = [refactorApi];
        } else {
          xvueApiMap[targetInstanceIndex]['apis'].push(refactorApi);
        }
      }
      app.config.globalProperties.$xvuePageComponentApiMap = [...xvueApiMap];
    },
    getApiDatas() {
      this.initApiDatas();
      const { apis } = this.config;
      this.apiReadyCount = 0;

      // 没加setTimeout的情况下watch不到apiReadyCount变化
      setTimeout(() => {
        this.apiReadyCount = 0;
        for (let i = 0; i < apis.length; i++) {
          const key = this.getApiKey(apis[i]);
          const api = this.apiDatas[key];
          if (api?.result && api.dataSourceType !== 'xVueApi') {
            this.apiReadyCount += 1;
            continue;
          }

          // 使用父组件传过来的数据
          if (api.useParentData) {
            this.apiDatas[key]['result'] = this.parentComp.resData || [];
            this.apiReadyCount += 1;
            continue;
          }

          const method = `getChartData${api.dataSourceType}`;
          const propertyMapping = this.getPropertyMapping(api);

          // 目前这种写法只适用于xMiner
          let params = {
            params: (this.params && this.params.params) || [],
            apiCode: api.apiCode,
            propertyMapping,
          };

          // 公用数据库增加传参
          if (this[`addCommonSourceParams${api.dataSourceType}`]) {
            params = this[`addCommonSourceParams${api.dataSourceType}`](params, api);
          }
          dataModel[method](params, api).then(res => {
            this.apiDatas[key]['result'] = res;
            this.apiReadyCount += 1;
          });
        }
      }, 20);
    },

    // xMiner公共数据库需要加上datasourceCode和isCommonDatasource参数
    addCommonSourceParamsxMinerPublicDatabase(params, data) {
      const { dataSource } = data;
      params.isCommonDatasource = true;
      params.datasourceCode = dataSource;
      return params;
    },

    replaceAsTooltip() {
      const nodes = this.$refs?.tipBox?.querySelectorAll?.('.asTooltip');
      for (let i = 0; i < nodes.length; i++) {
        const key = nodes[i].className.replace(/^.*asTooltip_([^\s]+).*$/, '$1');

        const hasTip = this.asTooltipUsers.find(n => n.asTooltipKey === key && n.wid === this.parentComp.widgetInstanceId);
        const domObj = window.asTooltipDoms && window.asTooltipDoms[key];

        if (domObj && hasTip) {
          nodes[i].innerHTML = '';
          nodes[i].appendChild(domObj.widget);
        } else {
          nodes[i].innerHTML = `@{${this.xssFilter(key)}}`;
        }
      }
    },
  },
};
</script>
<style lang="less" scoped>
.tooltip {
  width: 0;
  height: 0;
  display: none;
}
.tipBox {
  padding: 10px;
  border-radius: 3px;
  color: #fff;
  width: fit-content;
  &.dark {
    background-color: rgba(53, 83, 107, 0.7);
  }
  &.light {
    background-color: rgba(34, 34, 34, 0.7);
  }
}
</style>
